Skip to content

add taskmarket wallet#33

Merged
RedBeardEth merged 2 commits intomainfrom
wallet-change
Mar 9, 2026
Merged

add taskmarket wallet#33
RedBeardEth merged 2 commits intomainfrom
wallet-change

Conversation

@RedBeardEth
Copy link

@RedBeardEth RedBeardEth commented Mar 6, 2026

Summary by CodeRabbit

  • New Features

    • Added Taskmarket as a wallet/auth option alongside SAW, with onboarding prompts and sentinel-based credential support.
    • Installer now supports interactive wallet choice (SAW, Taskmarket, or both) and non-interactive defaults that install both.
  • Documentation

    • Updated installation and provider docs and quick-setup guidance to reflect multi-wallet flows and environment overrides.
  • Tests

    • Added comprehensive tests for Taskmarket wallet flows and small test formatting fixes.

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

Adds Taskmarket wallet authentication to Daydreams Router x402 alongside SAW and raw-key options. Updates installer and docs for interactive/non-interactive wallet selection, adds Taskmarket CLI support, implements Taskmarket keystore parsing/decryption and cached account resolution, updates payment backend to resolve Taskmarket on demand, and includes tests and dependency changes.

Changes

Cohort / File(s) Summary
Documentation
docs/install/index.md, docs/providers/x402.md
Switch docs to a wallet-agnostic onboarding narrative; document interactive TTY wallet choice (saw, taskmarket, both), non-interactive behavior, new Taskmarket CLI step, env overrides, and updated quick-setup JSON examples.
Installer Script
scripts/install-openclaw-fork.sh
Add interactive wallet-selection logic and non-TTY defaults; set SAW_INSTALL/TASKMARKET_INSTALL from selection or env; add optional Taskmarket CLI global install and TASKMARKET_BIN_PATH detection; update onboarding args and final messaging.
Auth Extension
extensions/daydreams-x402-auth/index.ts, extensions/daydreams-x402-auth/package.json
Introduce Taskmarket auth provider into x402 auth plugin: keystore prompts, device-key validation, sentinel generation, provider config patching; add viem dependency for account/signing support.
Taskmarket Wallet Module & Tests
src/agents/x402-taskmarket-wallet.ts, src/agents/x402-taskmarket-wallet.test.ts
New module: parse sentinel, load/validate keystore, fetch device encryption key, AES-256-GCM decrypt private key, validate address, cache accounts with retry/invalidation; expose testing helpers and clear cache; comprehensive unit tests added.
Payment Backend & Tests
src/agents/x402-payment.ts, src/agents/x402-payment.test.ts
Make signing backend resolution lazy and dynamic: prefer Taskmarket when configured, fallback to SAW or local key; add backend caching, logging, and error handling; minor test formatting tweaks.
Misc / Manifest
package.json, extensions/daydreams-x402-auth/package.json
Add viem dependency in extension package; manifest updates reflecting new files and tests.

Sequence Diagrams

sequenceDiagram
    participant User
    participant Installer as Installer Script
    participant AuthPlugin as x402 Auth Plugin
    participant CLI as Taskmarket CLI
    participant Keystore as Keystore (disk)
    participant DeviceAPI as Taskmarket Device API
    participant Wallet as x402-Taskmarket-Wallet

    User->>Installer: Run installer (TTY)
    Installer->>User: Prompt wallet choice (saw/taskmarket/both)
    User->>Installer: Select Taskmarket
    Installer->>AuthPlugin: Invoke onboarding with Taskmarket choice
    AuthPlugin->>CLI: Check taskmarket availability
    alt CLI missing
        AuthPlugin->>CLI: Run taskmarket init (provision)
        CLI->>Keystore: Create keystore file
    end
    AuthPlugin->>Keystore: Load keystore (encryptedKey, deviceId, walletAddress)
    AuthPlugin->>DeviceAPI: Request device encryption key (deviceId)
    DeviceAPI-->>AuthPlugin: Return DEK
    AuthPlugin->>Wallet: Decrypt encryptedKey with DEK
    Wallet-->>AuthPlugin: Decrypted private key + ownerAddress
    AuthPlugin->>AuthPlugin: Generate sentinel credential and patch config
    AuthPlugin-->>User: Onboarding complete (sentinel)
Loading
sequenceDiagram
    participant Client
    participant PaymentHandler as x402 Payment Handler
    participant Resolver as Backend Resolver
    participant TaskWallet as x402-Taskmarket-Wallet
    participant DeviceAPI as Taskmarket Device API
    participant Signer as Signer/Account

    Client->>PaymentHandler: fetch() with payment request
    PaymentHandler->>Resolver: resolveBackend()
    alt Taskmarket sentinel present
        Resolver->>TaskWallet: parse sentinel & createTaskmarketAccount()
        TaskWallet->>DeviceAPI: Fetch device encryption key
        DeviceAPI-->>TaskWallet: Return DEK
        TaskWallet->>TaskWallet: Decrypt private key (AES-256-GCM)
        TaskWallet->>Signer: Construct PrivateKeyAccount
        Signer-->>Resolver: Account + ownerAddress (cached)
    else SAW or local key
        Resolver->>Signer: Initialize SAW or local key backend
    end
    Resolver-->>PaymentHandler: Resolved backend
    PaymentHandler->>Signer: Sign request
    PaymentHandler-->>Client: Response with payment header
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A keystore tucked beneath my paw,
Taskmarket hums, the CLI I saw,
SAW waves hello, keys decrypt with cheer,
Three wallets dance and bring the router near,
Hoppy hops — onboarding done, hip hooray!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is entirely missing. The required template sections (Summary, Change Type, Scope, Security Impact, Repro + Verification, etc.) are completely absent. Complete the pull request description by filling in all required template sections, particularly Summary (problem/why it matters/what changed/scope boundaries), Change Type, Scope, Security Impact analysis, and Verification steps.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'add taskmarket wallet' is directly related to the main changes: introducing Taskmarket-based authentication and wallet integration throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch wallet-change

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/agents/x402-payment.ts (1)

805-821: Taskmarket backend resolves account per-request without promise caching.

Unlike SAW and private-key paths that cache via staticBackendPromise, the Taskmarket path calls createTaskmarketAccount() on every request. While createTaskmarketAccount has its own internal TTL-based cache, each call:

  1. Reads the keystore file from disk
  2. Checks the account cache
  3. Creates a new WalletClient instance

The account cache prevents redundant DEK fetches, but keystore reads and wallet client creations still occur per-request. If high throughput is expected, consider caching the resolved backend similar to the SAW path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agents/x402-payment.ts` around lines 805 - 821, The Taskmarket branch in
resolveBackend calls createTaskmarketAccount on every request causing filesystem
reads and new WalletClient creation; change it to cache the resolved
SigningBackend promise similarly to the SAW/private-key path (use the existing
staticBackendPromise pattern) so subsequent calls reuse the same promise/result
until it expires or owner changes; implement storing the promise when
taskmarketConfig is first used, reuse it on later resolveBackend calls, and when
the returned ownerAddress differs from taskmarketBackendAddress update
taskmarketBackendAddress, log the change, and invalidate/replace the cached
promise so a fresh backend is created.
docs/providers/x402.md (1)

25-29: Consider varying sentence structure for readability.

Three consecutive items start with "Phase". A minor style improvement would be to vary the phrasing:

📝 Suggested rewrite
-1. **Wallet choice**: if a TTY is available, the script asks whether to install SAW, Taskmarket, or both. In non-interactive shells it defaults to installing both.
-2. **Phase 1 — SAW daemon**: when selected, downloads SAW binaries, generates an EVM wallet key on-device, writes a conservative spending policy, and starts the daemon (systemd on Linux, LaunchAgent on macOS).
-3. **Phase 2 — OpenClaw gateway**: installs the CLI globally from a prebuilt release tarball (fast, no build tools needed).
-4. **Phase 2.25 — Taskmarket CLI**: when selected, installs the `taskmarket` binary globally so Taskmarket wallet onboarding can rely on it being present.
-5. **Phase 3 — Onboarding**: runs `openclaw onboard --auth-choice x402` if a TTY is available; otherwise prints the manual command.
+1. **Wallet choice**: if a TTY is available, the script asks whether to install SAW, Taskmarket, or both. In non-interactive shells it defaults to installing both.
+2. **SAW daemon** (Phase 1): when selected, downloads SAW binaries, generates an EVM wallet key on-device, writes a conservative spending policy, and starts the daemon (systemd on Linux, LaunchAgent on macOS).
+3. **OpenClaw gateway** (Phase 2): installs the CLI globally from a prebuilt release tarball (fast, no build tools needed).
+4. **Taskmarket CLI** (Phase 2.25): when selected, installs the `taskmarket` binary globally so Taskmarket wallet onboarding can rely on it being present.
+5. **Onboarding** (Phase 3): runs `openclaw onboard --auth-choice x402` if a TTY is available; otherwise prints the manual command.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/providers/x402.md` around lines 25 - 29, The three consecutive list
items starting with "Phase" reduce readability; update the bullet text in
docs/providers/x402.md so phrasing varies while preserving meaning and
referenced symbols: keep "Phase 1 — SAW daemon" as-is, reword "Phase 2 —
OpenClaw gateway" to something like "Install OpenClaw gateway (Phase 2)"
referencing the CLI install from the prebuilt release tarball, change "Phase
2.25 — Taskmarket CLI" to "Install Taskmarket CLI (Phase 2.25)" and keep its
note about installing the taskmarket binary globally, and rephrase "Phase 3 —
Onboarding" to "Onboarding (Phase 3)" while retaining the TTY behavior and the
exact command `openclaw onboard --auth-choice x402` for interactive shells;
ensure the sequence and technical details remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@extensions/daydreams-x402-auth/index.ts`:
- Around line 304-525: The listed utilities (resolveHomePath,
normalizeTaskmarketApiUrl, getErrorCode, decodeTaskmarketEncryptedKey,
decryptTaskmarketPrivateKey) are duplicated; extract them into a shared module
and have both this extension and src/agents/x402-taskmarket-wallet import that
module (or import from the core agents package), updating package dependency and
cross-package import paths to use .js if required; ensure the shared
decryptTaskmarketPrivateKey keeps the common logic and surface (throwing generic
Errors) and update callers like verifyTaskmarketWalletIntegrity and
verifyTaskmarketDeviceKeyAccess to use the centralized functions (preserve
names: resolveHomePath, normalizeTaskmarketApiUrl, getErrorCode,
decodeTaskmarketEncryptedKey, decryptTaskmarketPrivateKey).

---

Nitpick comments:
In `@docs/providers/x402.md`:
- Around line 25-29: The three consecutive list items starting with "Phase"
reduce readability; update the bullet text in docs/providers/x402.md so phrasing
varies while preserving meaning and referenced symbols: keep "Phase 1 — SAW
daemon" as-is, reword "Phase 2 — OpenClaw gateway" to something like "Install
OpenClaw gateway (Phase 2)" referencing the CLI install from the prebuilt
release tarball, change "Phase 2.25 — Taskmarket CLI" to "Install Taskmarket CLI
(Phase 2.25)" and keep its note about installing the taskmarket binary globally,
and rephrase "Phase 3 — Onboarding" to "Onboarding (Phase 3)" while retaining
the TTY behavior and the exact command `openclaw onboard --auth-choice x402` for
interactive shells; ensure the sequence and technical details remain unchanged.

In `@src/agents/x402-payment.ts`:
- Around line 805-821: The Taskmarket branch in resolveBackend calls
createTaskmarketAccount on every request causing filesystem reads and new
WalletClient creation; change it to cache the resolved SigningBackend promise
similarly to the SAW/private-key path (use the existing staticBackendPromise
pattern) so subsequent calls reuse the same promise/result until it expires or
owner changes; implement storing the promise when taskmarketConfig is first
used, reuse it on later resolveBackend calls, and when the returned ownerAddress
differs from taskmarketBackendAddress update taskmarketBackendAddress, log the
change, and invalidate/replace the cached promise so a fresh backend is created.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63de425e-9f5e-4a20-8f37-c13cbb0361fe

📥 Commits

Reviewing files that changed from the base of the PR and between 4fb6f8a and 2433b82.

📒 Files selected for processing (9)
  • docs/install/index.md
  • docs/providers/x402.md
  • extensions/daydreams-x402-auth/index.ts
  • extensions/daydreams-x402-auth/package.json
  • scripts/install-openclaw-fork.sh
  • src/agents/x402-payment.test.ts
  • src/agents/x402-payment.ts
  • src/agents/x402-taskmarket-wallet.test.ts
  • src/agents/x402-taskmarket-wallet.ts

Comment on lines +304 to +525
function normalizeTaskmarketApiUrl(value: string): string {
const raw = value.trim() || DEFAULT_TASKMARKET_API_URL;
const withProtocol = raw.startsWith("http") ? raw : `https://${raw}`;
return withProtocol.replace(/\/+$/, "");
}

function resolveHomePath(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
return trimmed;
}
if (trimmed === "~") {
return os.homedir();
}
if (trimmed.startsWith("~/")) {
return path.join(os.homedir(), trimmed.slice(2));
}
return path.resolve(trimmed);
}

function toBase64Url(value: string): string {
return Buffer.from(value, "utf8")
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
}

type TaskmarketKeystore = {
encryptedKey: string;
walletAddress: string;
deviceId: string;
apiToken: string;
};

function getErrorCode(error: unknown): string | undefined {
return error && typeof error === "object" && "code" in error && typeof error.code === "string"
? error.code
: undefined;
}

function buildTaskmarketSentinel(payload: { keystorePath: string; apiUrl?: string }): string {
const record: Record<string, unknown> = {
v: TASKMARKET_SENTINEL_VERSION,
keystorePath: payload.keystorePath,
};
const apiUrl = payload.apiUrl ? normalizeTaskmarketApiUrl(payload.apiUrl) : "";
if (apiUrl && apiUrl !== FALLBACK_TASKMARKET_API_URL) {
record.apiUrl = apiUrl;
}
return `${TASKMARKET_SENTINEL_PREFIX}${toBase64Url(JSON.stringify(record))}`;
}

function buildSawSentinel(walletName: string, socketPath: string): string {
return `saw:${walletName}@${socketPath}`;
}

async function loadTaskmarketKeystore(keystorePath: string): Promise<TaskmarketKeystore> {
const resolvedPath = resolveHomePath(keystorePath);
if (!resolvedPath) {
throw new Error("Taskmarket keystore path is required");
}

let raw: string;
try {
raw = await fs.readFile(resolvedPath, "utf8");
} catch (error) {
if (getErrorCode(error) === "ENOENT") {
throw new Error(
`Taskmarket keystore not found at ${resolvedPath}. Run taskmarket init first.`,
);
}
throw new Error(
`Taskmarket keystore at ${resolvedPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
);
}

let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
throw new Error(`Taskmarket keystore at ${resolvedPath} is not valid JSON.`);
}

const record = parsed as Record<string, unknown>;
const encryptedKey =
typeof record.encryptedKey === "string" ? record.encryptedKey.trim() : undefined;
const walletAddress =
typeof record.walletAddress === "string" ? record.walletAddress.trim() : undefined;
const deviceId = typeof record.deviceId === "string" ? record.deviceId.trim() : undefined;
const apiToken = typeof record.apiToken === "string" ? record.apiToken.trim() : undefined;
const normalizedWalletAddress = normalizeAddress(walletAddress);

if (!encryptedKey || !deviceId || !apiToken || !normalizedWalletAddress) {
throw new Error(
`Taskmarket keystore at ${resolvedPath} is missing required fields (encryptedKey, walletAddress, deviceId, apiToken).`,
);
}

return {
encryptedKey,
walletAddress: normalizedWalletAddress,
deviceId,
apiToken,
};
}

async function verifyTaskmarketDeviceKeyAccess(
apiUrl: string,
keystore: TaskmarketKeystore,
): Promise<string> {
const normalizedUrl = normalizeTaskmarketApiUrl(apiUrl);
const endpoint = `${normalizedUrl}/api/devices/${encodeURIComponent(keystore.deviceId)}/key`;
let response: Response;
try {
response = await fetch(endpoint, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ deviceId: keystore.deviceId, apiToken: keystore.apiToken }),
});
} catch (error) {
throw new Error(
`Could not contact Taskmarket API at ${normalizedUrl}: ${error instanceof Error ? error.message : String(error)}`,
);
}
if (!response.ok) {
const detail = await response.text().catch(() => "");
if (response.status === 401 || response.status === 403) {
throw new Error(
"Taskmarket device token was rejected. Reprovision wallet via taskmarket init and rerun onboarding.",
);
}
if (response.status === 404) {
throw new Error(
`Taskmarket device ${keystore.deviceId} was not found. Reprovision wallet via taskmarket init and rerun onboarding.`,
);
}
throw new Error(
`Taskmarket device key probe failed (${response.status}). ${detail.slice(0, 180)} Reprovision your wallet with taskmarket init if needed.`,
);
}

const parsed = (await response.json().catch(() => null)) as {
deviceEncryptionKey?: unknown;
} | null;
const dek =
parsed && typeof parsed.deviceEncryptionKey === "string"
? parsed.deviceEncryptionKey.trim()
: "";
if (!/^[0-9a-fA-F]{64}$/.test(dek)) {
throw new Error(
"Taskmarket device key probe returned an invalid key payload. Reprovision wallet via taskmarket init.",
);
}
return dek;
}

function decodeTaskmarketEncryptedKey(encryptedHex: string): {
iv: Buffer;
tag: Buffer;
ciphertext: Buffer;
} {
const data = Buffer.from(encryptedHex, "hex");
if (data.length <= 28) {
throw new Error("Taskmarket encrypted keystore payload is too short.");
}
const iv = data.subarray(0, 12);
const tag = data.subarray(12, 28);
const ciphertext = data.subarray(28);
if (ciphertext.length === 0) {
throw new Error("Taskmarket encrypted keystore payload is empty.");
}
return { iv, tag, ciphertext };
}

function decryptTaskmarketPrivateKey(deviceEncryptionKeyHex: string, encryptedHex: string): string {
const key = Buffer.from(deviceEncryptionKeyHex, "hex");
if (key.length !== 32) {
throw new Error(
"Taskmarket device key payload is invalid. Reprovision wallet via taskmarket init.",
);
}

const { iv, tag, ciphertext } = decodeTaskmarketEncryptedKey(encryptedHex);
try {
const decipher = createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString(
"utf8",
);
const normalized = decrypted.trim().startsWith("0X")
? `0x${decrypted.trim().slice(2)}`
: decrypted.trim();
if (!PRIVATE_KEY_REGEX.test(normalized)) {
throw new Error("Decrypted Taskmarket key is not a valid private key.");
}
return normalized;
} catch (error) {
if (
error instanceof Error &&
error.message === "Decrypted Taskmarket key is not a valid private key."
) {
throw error;
}
throw new Error(
"Taskmarket keystore could not be decrypted with the fetched device key. Reprovision wallet via taskmarket init.",
);
}
}

function verifyTaskmarketWalletIntegrity(
keystore: TaskmarketKeystore,
deviceEncryptionKey: string,
): void {
const privateKey = decryptTaskmarketPrivateKey(deviceEncryptionKey, keystore.encryptedKey);
const account = privateKeyToAccount(privateKey as `0x${string}`);
if (account.address.toLowerCase() !== keystore.walletAddress.toLowerCase()) {
throw new Error(
"Taskmarket keystore address mismatch after decryption. Reprovision wallet via taskmarket init and rerun onboarding.",
);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if both files exist
echo "=== Checking file existence ==="
ls -lh extensions/daydreams-x402-auth/index.ts src/agents/x402-taskmarket-wallet.ts 2>&1

# Extract function names from both files
echo -e "\n=== Functions in x402-taskmarket-wallet.ts ==="
rg -n "^(export\s+)?(async\s+)?function\s+\w+" src/agents/x402-taskmarket-wallet.ts | head -30

echo -e "\n=== Functions in daydreams-x402-auth/index.ts ==="
rg -n "^(export\s+)?(async\s+)?function\s+\w+" extensions/daydreams-x402-auth/index.ts | head -30

Repository: daydreamsai/dreaming-claw

Length of output: 2918


🏁 Script executed:

#!/bin/bash
# Get specific function implementations to compare
echo "=== resolveHomePath in x402-taskmarket-wallet.ts ==="
sed -n '/^function resolveHomePath/,/^}/p' src/agents/x402-taskmarket-wallet.ts

echo -e "\n=== resolveHomePath in daydreams-x402-auth/index.ts ==="
sed -n '/^function resolveHomePath/,/^}/p' extensions/daydreams-x402-auth/index.ts

echo -e "\n=== normalizeTaskmarketApiUrl in x402-taskmarket-wallet.ts ==="
sed -n '/^function normalizeTaskmarketApiUrl/,/^}/p' src/agents/x402-taskmarket-wallet.ts

echo -e "\n=== normalizeTaskmarketApiUrl in daydreams-x402-auth/index.ts ==="
sed -n '/^function normalizeTaskmarketApiUrl/,/^}/p' extensions/daydreams-x402-auth/index.ts

Repository: daydreamsai/dreaming-claw

Length of output: 1152


🏁 Script executed:

#!/bin/bash
# Check imports at the top of both files to understand dependencies
echo "=== Imports in x402-taskmarket-wallet.ts (first 30 lines) ==="
head -30 src/agents/x402-taskmarket-wallet.ts

echo -e "\n=== Imports in daydreams-x402-auth/index.ts (first 40 lines) ==="
head -40 extensions/daydreams-x402-auth/index.ts

Repository: daydreamsai/dreaming-claw

Length of output: 2574


🏁 Script executed:

#!/bin/bash
# Compare the normalizeApiUrl vs normalizeTaskmarketApiUrl
echo "=== normalizeApiUrl in x402-taskmarket-wallet.ts ==="
sed -n '/^function normalizeApiUrl/,/^}/p' src/agents/x402-taskmarket-wallet.ts

echo -e "\n=== decodeTaskmarketEncryptedKey in x402-taskmarket-wallet.ts ==="
sed -n '/^function decodeTaskmarketEncryptedKey/,/^}/p' src/agents/x402-taskmarket-wallet.ts | head -30

echo -e "\n=== decodeTaskmarketEncryptedKey in daydreams-x402-auth/index.ts ==="
sed -n '/^function decodeTaskmarketEncryptedKey/,/^}/p' extensions/daydreams-x402-auth/index.ts | head -30

echo -e "\n=== decryptTaskmarketPrivateKey in x402-taskmarket-wallet.ts ==="
sed -n '/^function decryptTaskmarketPrivateKey/,/^}/p' src/agents/x402-taskmarket-wallet.ts | head -30

echo -e "\n=== decryptTaskmarketPrivateKey in daydreams-x402-auth/index.ts ==="
sed -n '/^function decryptTaskmarketPrivateKey/,/^}/p' extensions/daydreams-x402-auth/index.ts | head -30

Repository: daydreamsai/dreaming-claw

Length of output: 3023


🏁 Script executed:

#!/bin/bash
# Check getErrorCode and loadTaskmarketKeystore
echo "=== getErrorCode in x402-taskmarket-wallet.ts ==="
sed -n '/^function getErrorCode/,/^}/p' src/agents/x402-taskmarket-wallet.ts

echo -e "\n=== getErrorCode in daydreams-x402-auth/index.ts ==="
sed -n '/^function getErrorCode/,/^}/p' extensions/daydreams-x402-auth/index.ts

echo -e "\n=== loadTaskmarketKeystore signatures ==="
echo "x402-taskmarket-wallet.ts:"
grep -A 2 "^async function loadTaskmarketKeystore" src/agents/x402-taskmarket-wallet.ts

echo -e "\ndaydreams-x402-auth/index.ts:"
grep -A 2 "^async function loadTaskmarketKeystore" extensions/daydreams-x402-auth/index.ts

Repository: daydreamsai/dreaming-claw

Length of output: 959


🏁 Script executed:

#!/bin/bash
# Check directory structure to understand cross-package import feasibility
echo "=== Directory structure ==="
ls -la extensions/daydreams-x402-auth/ | head -15
echo -e "\n=== Package boundaries ==="
cat extensions/daydreams-x402-auth/package.json 2>/dev/null | head -20
cat src/agents/package.json 2>/dev/null | head -20
</parameter>

Repository: daydreamsai/dreaming-claw

Length of output: 870


Extract shared Taskmarket utilities to avoid code duplication.

The following functions are duplicated between this file and src/agents/x402-taskmarket-wallet.ts:

  • resolveHomePath (identical)
  • normalizeApiUrl / normalizeTaskmarketApiUrl (identical logic)
  • getErrorCode (identical)
  • decodeTaskmarketEncryptedKey (identical)
  • decryptTaskmarketPrivateKey (identical core logic, differs only in error type)

This violates the DRY principle. Consider either importing these utilities from the core module or extracting them to a shared module that both can depend on. Note that the extension package currently doesn't depend on the core agents module, so this would require adding that dependency and using .js extensions for cross-package imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/daydreams-x402-auth/index.ts` around lines 304 - 525, The listed
utilities (resolveHomePath, normalizeTaskmarketApiUrl, getErrorCode,
decodeTaskmarketEncryptedKey, decryptTaskmarketPrivateKey) are duplicated;
extract them into a shared module and have both this extension and
src/agents/x402-taskmarket-wallet import that module (or import from the core
agents package), updating package dependency and cross-package import paths to
use .js if required; ensure the shared decryptTaskmarketPrivateKey keeps the
common logic and surface (throwing generic Errors) and update callers like
verifyTaskmarketWalletIntegrity and verifyTaskmarketDeviceKeyAccess to use the
centralized functions (preserve names: resolveHomePath,
normalizeTaskmarketApiUrl, getErrorCode, decodeTaskmarketEncryptedKey,
decryptTaskmarketPrivateKey).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@extensions/daydreams-x402-auth/index.ts`:
- Around line 388-401: Validate encryptedKey more strictly immediately after
parsing: when constructing encryptedKey from parsed/record, ensure it matches
the same hex format and minimum length checks used in
src/agents/x402-taskmarket-wallet.ts (e.g., only [0-9a-fA-F] and required
byte/char length) before calling normalizeAddress or performing any
network/device operations; if the check fails, throw a clear Error mentioning
the keystore and that encryptedKey is malformed so the failure is local and not
treated as an onboarding/device error. Reference variables: parsed/record,
encryptedKey, normalizeAddress.
- Around line 833-835: The code currently calls ensureTaskmarketCliAvailable()
unconditionally before checking for an existing keystore, which incorrectly
forces the CLI even when the JSON keystore is present; remove that top-level
call and instead call ensureTaskmarketCliAvailable() only inside the branch in
ensureTaskmarketWalletProvisioned() that handles creating/provisioning the
keystore (i.e., the "missing-file" path), so
loadTaskmarketKeystore(keystorePath) can succeed without the CLI when the
keystore already exists.
- Around line 304-307: normalizeTaskmarketApiUrl currently accepts and upgrades
bare hostnames but does not reject explicit http:// endpoints; update this
function to parse the final URL (use the URL constructor) after normalizing
(default to DEFAULT_TASKMARKET_API_URL and prepend https:// when missing), and
if url.protocol === "http:" then throw a clear error unless url.hostname is an
allowed localhost host (e.g. "localhost", "127.0.0.1", "::1") so local dev still
works; reference normalizeTaskmarketApiUrl and DEFAULT_TASKMARKET_API_URL when
making this change.

In `@src/agents/x402-taskmarket-wallet.ts`:
- Around line 80-83: The normalizeApiUrl function currently accepts plain http
and can leak credentials; update normalizeApiUrl to reject non-HTTPS URLs by
parsing the input (function normalizeApiUrl) and throwing an error if the scheme
is "http" or missing and would result in "http://" except allow explicit
localhost/127.0.0.1 (and [::1]) addresses as an explicit dev exception;
normalize to https:// for other hosts and still strip trailing slashes. Apply
the same strict HTTPS guard to the auth extension’s copy of this normalizer so
both places behave identically.
- Around line 246-260: The POST in fetchTaskmarketDeviceEncryptionKey can hang;
add a request deadline by using an AbortController and passing its signal to
params.fetchFn, start a timeout (e.g., 5–10s) that calls controller.abort(), and
clear the timeout once the fetch resolves; catch the abort/timeout case
(AbortError) and throw a descriptive error so createTaskmarketAccount() can
retry. Ensure the AbortController is created inside
fetchTaskmarketDeviceEncryptionKey, the timeout is cleared after response or
error, and the abort signal is included in the fetch options with existing
headers/body logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c7c3629-6134-4f74-9b13-0a8f8a4dddcd

📥 Commits

Reviewing files that changed from the base of the PR and between 2433b82 and 03defc3.

📒 Files selected for processing (3)
  • extensions/daydreams-x402-auth/index.ts
  • src/agents/x402-taskmarket-wallet.test.ts
  • src/agents/x402-taskmarket-wallet.ts

Comment on lines +304 to +307
function normalizeTaskmarketApiUrl(value: string): string {
const raw = value.trim() || DEFAULT_TASKMARKET_API_URL;
const withProtocol = raw.startsWith("http") ? raw : `https://${raw}`;
return withProtocol.replace(/\/+$/, "");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce HTTPS for Taskmarket API URLs during onboarding.

If TASKMARKET_API_URL is set to http://..., this flow will post the device token and receive the device key over cleartext. Reject non-HTTPS endpoints here, with an explicit localhost exception if you need local development.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/daydreams-x402-auth/index.ts` around lines 304 - 307,
normalizeTaskmarketApiUrl currently accepts and upgrades bare hostnames but does
not reject explicit http:// endpoints; update this function to parse the final
URL (use the URL constructor) after normalizing (default to
DEFAULT_TASKMARKET_API_URL and prepend https:// when missing), and if
url.protocol === "http:" then throw a clear error unless url.hostname is an
allowed localhost host (e.g. "localhost", "127.0.0.1", "::1") so local dev still
works; reference normalizeTaskmarketApiUrl and DEFAULT_TASKMARKET_API_URL when
making this change.

Comment on lines +388 to +401
const record = parsed as Record<string, unknown>;
const encryptedKey =
typeof record.encryptedKey === "string" ? record.encryptedKey.trim() : undefined;
const walletAddress =
typeof record.walletAddress === "string" ? record.walletAddress.trim() : undefined;
const deviceId = typeof record.deviceId === "string" ? record.deviceId.trim() : undefined;
const apiToken = typeof record.apiToken === "string" ? record.apiToken.trim() : undefined;
const normalizedWalletAddress = normalizeAddress(walletAddress);

if (!encryptedKey || !deviceId || !apiToken || !normalizedWalletAddress) {
throw new Error(
`Taskmarket keystore at ${resolvedPath} is missing required fields (encryptedKey, walletAddress, deviceId, apiToken).`,
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tighten encryptedKey validation before the network probe.

This copy only checks that encryptedKey is present. Non-hex or too-short payloads will fall through to a later decrypt/device-key failure, which turns a local keystore problem into a misleading onboarding error. The stricter hex/length checks in src/agents/x402-taskmarket-wallet.ts should be mirrored here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/daydreams-x402-auth/index.ts` around lines 388 - 401, Validate
encryptedKey more strictly immediately after parsing: when constructing
encryptedKey from parsed/record, ensure it matches the same hex format and
minimum length checks used in src/agents/x402-taskmarket-wallet.ts (e.g., only
[0-9a-fA-F] and required byte/char length) before calling normalizeAddress or
performing any network/device operations; if the check fails, throw a clear
Error mentioning the keystore and that encryptedKey is malformed so the failure
is local and not treated as an onboarding/device error. Reference variables:
parsed/record, encryptedKey, normalizeAddress.

Comment on lines +833 to +835
ensureTaskmarketCliAvailable();
await ensureTaskmarketWalletProvisioned(ctx, keystorePath);
const keystore = await loadTaskmarketKeystore(keystorePath);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t require the Taskmarket CLI when the keystore already exists.

taskmarket init is the only CLI-dependent step, but this check runs before you even look for the keystore. That blocks otherwise valid setups where the JSON file is already present and the runtime path works without the CLI. Move the availability check into the missing-file branch inside ensureTaskmarketWalletProvisioned().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/daydreams-x402-auth/index.ts` around lines 833 - 835, The code
currently calls ensureTaskmarketCliAvailable() unconditionally before checking
for an existing keystore, which incorrectly forces the CLI even when the JSON
keystore is present; remove that top-level call and instead call
ensureTaskmarketCliAvailable() only inside the branch in
ensureTaskmarketWalletProvisioned() that handles creating/provisioning the
keystore (i.e., the "missing-file" path), so
loadTaskmarketKeystore(keystorePath) can succeed without the CLI when the
keystore already exists.

Comment on lines +80 to +83
function normalizeApiUrl(value: string): string {
const raw = value.trim() || DEFAULT_TASKMARKET_API_URL;
const withProtocol = raw.startsWith("http") ? raw : `https://${raw}`;
return withProtocol.replace(/\/+$/, "");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject plaintext Taskmarket API URLs.

http://... is accepted unchanged here, so a sentinel or env override can send the device token and returned device key over cleartext. Please fail closed for non-HTTPS endpoints, with an explicit localhost exception if local dev needs it. The auth extension’s copy of this normalizer needs the same guard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agents/x402-taskmarket-wallet.ts` around lines 80 - 83, The
normalizeApiUrl function currently accepts plain http and can leak credentials;
update normalizeApiUrl to reject non-HTTPS URLs by parsing the input (function
normalizeApiUrl) and throwing an error if the scheme is "http" or missing and
would result in "http://" except allow explicit localhost/127.0.0.1 (and [::1])
addresses as an explicit dev exception; normalize to https:// for other hosts
and still strip trailing slashes. Apply the same strict HTTPS guard to the auth
extension’s copy of this normalizer so both places behave identically.

Comment on lines +246 to +260
async function fetchTaskmarketDeviceEncryptionKey(params: {
apiUrl: string;
deviceId: string;
apiToken: string;
fetchFn: typeof fetch;
}): Promise<string> {
const apiUrl = normalizeApiUrl(params.apiUrl);
const url = `${apiUrl}/api/devices/${encodeURIComponent(params.deviceId)}/key`;
let response: Response;
try {
response = await params.fetchFn(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ deviceId: params.deviceId, apiToken: params.apiToken }),
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a deadline to the device-key request.

If this POST stalls, createTaskmarketAccount() can hang indefinitely on the request path and the retry logic never runs. Please wrap the request in a timeout/cancellation path so network wedges fail fast.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agents/x402-taskmarket-wallet.ts` around lines 246 - 260, The POST in
fetchTaskmarketDeviceEncryptionKey can hang; add a request deadline by using an
AbortController and passing its signal to params.fetchFn, start a timeout (e.g.,
5–10s) that calls controller.abort(), and clear the timeout once the fetch
resolves; catch the abort/timeout case (AbortError) and throw a descriptive
error so createTaskmarketAccount() can retry. Ensure the AbortController is
created inside fetchTaskmarketDeviceEncryptionKey, the timeout is cleared after
response or error, and the abort signal is included in the fetch options with
existing headers/body logic.

@RedBeardEth RedBeardEth merged commit 7b5bbab into main Mar 9, 2026
15 of 22 checks passed
@RedBeardEth RedBeardEth deleted the wallet-change branch March 9, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant